/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.xre.protocol.testing.netty.http;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;

import com.hercules.xre.protocol.testing.EventHandler;
import com.hercules.xre.protocol.testing.EventHandlerException;
import com.hercules.xre.protocol.testing.Helper;
import com.hercules.xre.protocol.testing.netty.CommandSender;
import com.hercules.xre.protocol.testing.netty.CommandsDeliveryStatusFutureListener;
import com.hercules.xre.protocol.testing.netty.IllegalMessageException;
import com.hercules.xre.protocol.testing.netty.NoSuchSessionException;
import com.topcoder.util.log.Level;
import com.topcoder.util.log.Log;
import com.topcoder.util.log.LogManager;
/**
 * <p>
 * Channel handler for messages received trough HTTP connection.
 * This handler should be preceded in the pipeline by  HTTPResponseEncoder.
 * This class retrieves POST parameters passed with a request and retrieves
 * value of parameter that should contain serialized events. Retrieved String
 * value is then passed up in the pipeline to next message handler.
 * </p>
 * <p>
 * <strong>Thread-safety:</strong> This class uses synchronized collections in order to assure thread safety.
 * </p>
 * @author TCSDESIGNER, TCSDEVELOPER
 * @version 1.0
*/
@ChannelPipelineCoverage("one")
public class HTTPCommandHandler extends SimpleChannelHandler implements CommandSender {
    /**
     * Content type of send HTTP Responses.
     */
    private static final  String CONTENT_TYPE = "application/x-xre";

    /**
     * <p>
     * Logger that is used to log activities and errors.
     * It is initialized to LogManager.getLog(HTTPCommandHandler.class.getName()).
     * Can not be null, never changes after initialization.
     * </p>
     */
    private static final Log LOGGER = LogManager.getLog(HTTPCommandHandler.class.getName());

    /**
     * <p>
     * Event handler that will be used by channel handlers to inform application about
     * communication events.
     * </p>
     * <p>
     * It is set in the constructor and never changes after that. Can't be null.
     * </p>
     */
    private final EventHandler eventHandler;

    /**
     * <p>
     * Connection manager that will be used in the command handler.
     * </p>
     * <p>
     * It is set in the constructor and never changes after that. Can't be null.
     * </p>
     */
    private final HTTPProtocolTestingConnectionManager  connectionManager;

    /**
     * <p>
     * List of all commands that should be sent when next HTTPRequest arrives.
     * Elements are added to this List in writeRequest()
     * Elements are read and removed from this list in messageReceived() method
     * Elements are read in channelDisconnected method
     * Can't be null, may be empty, can't contain null/empty string.
     * </p>
     */
    private final List<String> pendingCommands = Collections.synchronizedList(new ArrayList<String>());

    /**
     * <p>
     * Set of all commands that are being sent to the client but framework hasn't
     * reported yet that delivery succeeded.
     * Elements are added to this List in messageReceived() method.
     * Elements are removed from this list in commandsDelivered()  method
     * Elements are read in channelDisconnected method
     * Can't be null, may be empty, can't contain null/empty string.
     * </p>
     */
    private final Set<String> commandsBeingSent =  Collections.synchronizedSet(new HashSet<String>());

    /**
     * <p>
     * Creates new instance.
     * </p>
     * @param eventHandler event handler that will be used to report detected events, can't be null
     * @param connectionManager connection manager, can't be null
     * @throws IllegalArgumentException when any attribute is invalid
     */
    public HTTPCommandHandler(HTTPProtocolTestingConnectionManager connectionManager, EventHandler eventHandler) {
        final String signature = "HTTPCommandHandler#ctor";
        Helper.logEnter(signature, new String[] {"connectionManager", "callback"},
            new Object[] {connectionManager, eventHandler}, LOGGER);
        try {
            Helper.checkNull(connectionManager, "connectionManager", signature, LOGGER);
            Helper.checkNull(eventHandler, "eventHandler", signature, LOGGER);
            this.connectionManager = connectionManager;
            this.eventHandler = eventHandler;
        } finally {
            Helper.logExit(signature, null, LOGGER);
        }
    }

    /**
     * <p>
     * Method called when HTTPRequest has been received and processed.
     * Now HTTPResponse should be prepared and this method is responsible for that.
     * It serializes all pending commands, inserts them into http response body,
     * sets appropriate header elements and sends the response.
     * </p>
     * <p>
     * Please note that in order to monitor command delivery status all commands that are
     * being sent are added to commandsBeingSent set, After successful delivery
     * commandsDelivered() method will be called in order to remove delivered commands from
     * this set.
     * </p>
     * @param e message event, can't be null
     * @param ctx channel handler context, can't be null
     * @throws IllegalArgumentException when any of parameters is invalid.
     * @throws IllegalMessageException if the event string is invalid
    */
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
        throws IllegalMessageException {
        final String signature = "HTTPCommandHandler#messageReceived";
        Helper.logEnter(signature, new String[] {"ChannelHandlerContext", "MessageEvent"},
            new Object[] {ctx, e}, LOGGER);
        try {
            Helper.checkNull(ctx, "ctx", signature, LOGGER);
            Helper.checkNull(e, "Message Event", signature, LOGGER);
            // prepare a HttpResponse
            HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            // set content type
            response.setHeader(HttpHeaders.Names.CONTENT_TYPE, CONTENT_TYPE);
            // set keep alive
            response.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            // prepare string representing all pending commands
            List<String> commandsToBeSent = new ArrayList<String>();
            synchronized (pendingCommands) {
                commandsToBeSent.addAll(pendingCommands);
                // clear commands cache
                pendingCommands.clear();
            }

            StringBuffer sb = new StringBuffer("[");
            for (String command : commandsToBeSent) {
                sb.append(command).append(",");
            }

            sb.replace(sb.length() - 1, sb.length(), "]");

            ChannelBuffer buffer = ChannelBuffers.copiedBuffer(sb.toString(), "UTF-8");
            response.setContent(buffer);
            response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buffer.readableBytes()));

            // add future listener that will monitor command delivery status
            ChannelFuture future = e.getFuture();

            if (future == null) {
                // Note : Channels#write will throw NPE if future is null. So we must check it first
                throw Helper.logError(new IllegalMessageException(
                    "future can't be null"), signature, LOGGER);
            }
            if (ctx.getChannel() == null) {
                // Note : Channels#write will throw NPE if channel is null. So we must check it first
                throw Helper.logError(new IllegalMessageException(
                    "Channel of ChannelHandlerContext can't be null"), signature, LOGGER);
            }
            // send write event (with prepared response as a message) to
            // previous handler in the pipeline (now the message goes down, as it is written)
            Channels.write(ctx, e.getFuture(), response);
            // Add commands to set of commands that are being sent to control if they have been successfully delivered
            if (!commandsToBeSent.isEmpty()) {
                commandsBeingSent.addAll(commandsToBeSent);
                future.addListener(new CommandsDeliveryStatusFutureListener(this, commandsToBeSent));
            }
            LOGGER.log(Level.TRACE, "sending commands : " + sb.toString());
        } finally {
            Helper.logExit(signature, null, LOGGER);
        }
    }

    /**
     * <p>
     * Method called when new command is to be sent to a client.
     * As data to the client can be sent only in response to his request,
     * passed command is added to pendingCommands list.
     * </p>
     * <p>
     * This method will use connectionManager to replace the command index and timestamp
     * properties of the command to be sent.
     * </p>
     * @param e message event, can't be null
     * @param ctx channel handler context, can't be null
     * @throws IllegalArgumentException when any of parameters is invalid.
     * @throws IllegalMessageException when there's no commandIndex/timestamp
     *         in the parsed command(in the message event)
     * @throws NoSuchSessionException if no specific session was found.
    */
    public void writeRequested(ChannelHandlerContext ctx, MessageEvent e)
        throws IllegalMessageException, NoSuchSessionException {
        final String signature = "HTTPCommandHandler#writeRequested";
        Helper.logEnter(signature, new String[] {"ChannelHandlerContext", "MessageEvent"},
            new Object[] {ctx, e}, LOGGER);
        try {
            Helper.checkNull(ctx, "ctx", signature, LOGGER);
            Helper.checkNull(e, "Message Event", signature, LOGGER);
            String command = (String) e.getMessage();

            command = connectionManager.prepareCommandToSend(command, ctx.getChannel());
            pendingCommands.add(command);
        } catch (NoSuchSessionException ex) {
            throw Helper.logError(ex, signature, LOGGER);
        } catch (IllegalMessageException ex) {
            throw Helper.logError(ex, signature, LOGGER);
        } finally {
            Helper.logExit(signature, null, LOGGER);
        }
    }

    /**
     * <p>
     * Channel state event handler called when channel has been disconnected from its peer.
     * This method informs (using eventHandler) application about encountered situation.
     * </p>
     * @param e message event, can't be null
     * @param ctx channel handler context, can't be null
     * @throws IllegalArgumentException when any of parameters is invalid.
     * @throws NoSuchSessionException if no specific session was found.
    */
    public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e)
        throws NoSuchSessionException {
        final String signature = "HTTPCommandHandler#channelDisconnected";
        Helper.checkNull(ctx, "ctx", signature, LOGGER);
        Helper.checkNull(e, "Message Event", signature, LOGGER);

        // get session GUID assigned to current channel
        try {
            // get session GUID assigned to current channel
            Channel channel = ctx.getChannel();
            if (channel == null) {
                throw Helper.logError(new NoSuchSessionException("Can not get the channel"), signature, LOGGER);
            }
            String sessionGUID = connectionManager.getSessionGUIDByChannel(channel);
            // copy contents of pandingCommands synchronized list into normal list
            List<String> commandsNotDelivered = new ArrayList<String>(pendingCommands);
            // commands that are in "commandsBeingSent" set also haven't been delivered, add them to the list
            commandsNotDelivered.addAll(commandsBeingSent);
            eventHandler.handleConnectionDisconnected(sessionGUID, commandsNotDelivered);
        } catch (NoSuchSessionException ex) {
            throw Helper.logError(ex, signature, LOGGER);
        } catch (EventHandlerException ex) {
            Helper.logError(ex, signature, LOGGER);
        }
    }

    /**
     * <p>
     * Method called when command have been successfully delivered.
     * </p>
     * @param commands list of delivered commands, can't be null
     * @throws IllegalArgumentException when parameter is null
    */
    public void commandsDelivered(List<String> commands) {
        final String signature = "HTTPCommandHandler#commandsDelivered";
        Helper.logEnter(signature, new String[] {"commands"}, new Object[] {commands}, LOGGER);
        try {
            Helper.checkList(commands, "commands", signature, LOGGER);
            commandsBeingSent.removeAll(commands);
        } finally {
            Helper.logExit(signature, null, LOGGER);
        }
    }
}

